#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]

use std::{
    env,
    process::{Child, Command},
    thread,
    time::Duration,
};

use coverage_helper::test;
use cs2s::{
    cache::{CacheTableClientHandle, CacheTableKeyRef, CacheTableVal},
    command::CommandQueueClientHandle,
    route::LookupRouter,
    utils::mangle::PathStrExt,
};
use cs2s_testdata::*;

/// Try doing something in a loop, with sleep in between.
macro_rules! try_loop {
    ($count:expr => $loop:block) => {{
        let mut __i = 0;
        let __threshold = $count;
        loop {
            let __result = $loop;
            if __result.is_ok() || __i >= __threshold {
                break __result.unwrap();
            } else {
                __i += 1;
                thread::sleep(Duration::from_secs(1));
            }
        }
    }};
}

/// Create a command to start the server.
macro_rules! server_cmd {
    ($ident:ident (no-prefix): $name:expr $(, $args:expr)*) => {{
        server_cmd!(__internal $ident: $name $(, $args)*)
    }};

    ($ident:ident: $name:expr $(, $args:expr)*) => {{
        server_cmd!(__internal $ident: &format!("{}-{}", PREFIX, $name) $(, $args)*)
    }};

    (__internal $ident:ident: $name:expr $(, $args:expr)*) => {{
        let mut command = Command::new(PATH);
        command.args(["--name", $name, "server", stringify!($ident)]);
        $(
            command.arg($args);
        )*
        command.env("CS2S_CTL_LOG_LEVEL", "trace");
        command
    }};
}

#[test]
fn start_and_stop() {
    // Start the server.
    let server = server_cmd!(start: "start-and-stop").spawn().unwrap();
    thread::sleep(Duration::from_secs(1));

    // Stop the server.
    assert_stop("start-and-stop", server);
}

#[test]
fn basic_interaction() {
    const NAME: &str = "basic-interaction";

    // Start the server.
    let server = server_cmd!(start: NAME).spawn().unwrap();

    // Create a client.
    let mut client = try_loop!(3 => {
        CommandQueueClientHandle::new(&queue_name(NAME))
    });

    let mut client_router = LookupRouter::new(&route_name(NAME));

    // Connect to the server.
    client.connect().unwrap();

    // Attach paths.
    client.paths_attach(PATHS).unwrap();
    try_loop!(3 => {
        client_router.attach(PATHS)
    });

    // Assert that the cache tables are empty.
    for path in PATHS {
        for (i, guest_code) in GUEST_CODES.iter().enumerate() {
            assert!(client_router
                .lookup(
                    path,
                    &CacheTableKeyRef {
                        guest_addr: 0x400000 + i * 0x40,
                        guest_size: guest_code.len(),
                        guest_sign: &guest_code.into(),
                    }
                )
                .unwrap()
                .is_none());
        }
    }

    // Synchronize cache.
    for (i, (path, (guest_code, (host_meta, host_code)))) in PATHS
        .iter()
        .zip(
            GUEST_CODES
                .iter()
                .zip(HOST_METAS.iter().zip(HOST_CODES.iter())),
        )
        .enumerate()
    {
        assert!(client
            .synchronize(
                path,
                &CacheTableKeyRef {
                    guest_addr: 0x400000 + i * 0x40,
                    guest_size: guest_code.len(),
                    guest_sign: &guest_code.into(),
                },
                &CacheTableVal {
                    host_meta,
                    host_code
                },
            )
            .is_ok());
    }

    // Assert that the cache tables are filled.
    for (i, (path, guest_code)) in PATHS.iter().zip(GUEST_CODES.iter()).enumerate() {
        let guest_sign = guest_code.into();

        try_loop!(3 => {
            client_router
                .lookup(
                    path,
                    &CacheTableKeyRef {
                        guest_addr: 0x400000 + i * 0x40,
                        guest_size: guest_code.len(),
                        guest_sign: &guest_sign,
                    }
                )
                .unwrap()
                .ok_or("Lookup failed")
        });
    }

    // Stop the server.
    assert_stop(NAME, server);
}

#[test]
fn duplicated_path_attach() {
    const NAME: &str = "duplicated-path-attach";

    // Start the server.
    let server = server_cmd!(start: NAME).spawn().unwrap();

    // Create a client.
    let mut client = try_loop!(3 => {
        CommandQueueClientHandle::new(&queue_name(NAME))
    });

    // Connect to the server.
    client.connect().unwrap();

    // Attach paths twice.
    client.paths_attach(PATHS).unwrap();
    client.paths_attach(PATHS).unwrap();

    // Assert that the server has created cache tables.
    for path in PATHS {
        let mangled_path = path.mangle();

        try_loop!(3 => {
            CacheTableClientHandle::new(&table_name(NAME, &mangled_path))
        });
    }

    // Stop the server.
    assert_stop(NAME, server);
}

#[test]
fn unattached_path() {
    const NAME: &str = "unattached-path";

    // Start the server.
    let server = server_cmd!(start: NAME).spawn().unwrap();

    // Create a client.
    let mut client = try_loop!(3 => {
        CommandQueueClientHandle::new(&queue_name(NAME))
    });

    // Connect to the server.
    client.connect().unwrap();

    // Synchronize cache belonging to an unattached path.
    client
        .synchronize(
            PATH_0,
            &CacheTableKeyRef {
                guest_addr: 0x400000,
                guest_size: GUEST_CODE_0.len(),
                guest_sign: &GUEST_CODE_0.into(),
            },
            &CacheTableVal {
                host_meta: &HOST_META_0,
                host_code: &HOST_CODE_0,
            },
        )
        .unwrap();

    // Stop the server.
    assert_stop(NAME, server);
}

#[test]
fn path_existence() {
    const NAME: &str = "path-existence";
    const NONEXISTENT_PATH: &str = "/lib/libnonexist!.so";

    // Start the server.
    let server = server_cmd!(start: NAME).spawn().unwrap();

    // Create a client.
    let mut client = try_loop!(3 => {
        CommandQueueClientHandle::new(&queue_name(NAME))
    });

    // Connect to the server.
    client.connect().unwrap();

    // Attach a nonexistent path.
    client.paths_attach([NONEXISTENT_PATH]).unwrap();

    // Synchronize cache belonging to the nonexistent path.
    client
        .synchronize(
            NONEXISTENT_PATH,
            &CacheTableKeyRef {
                guest_addr: 0x400000,
                guest_size: GUEST_CODE_0.len(),
                guest_sign: &GUEST_CODE_0.into(),
            },
            &CacheTableVal {
                host_meta: &HOST_META_0,
                host_code: &HOST_CODE_0,
            },
        )
        .unwrap();

    // Attach an existent path.
    client.paths_attach([PATH]).unwrap();

    // Stop the server.
    assert_stop(NAME, server);
}

#[test]
fn congestion() {
    const NAME: &str = "congestion";

    // Start the server.
    let server = server_cmd!(start: NAME, "-T", "0.1").spawn().unwrap();

    // Create a client.
    let mut client = try_loop!(3 => {
        CommandQueueClientHandle::new(&queue_name(NAME))
    });

    // (Cmd #1) Connect to the server.
    client.connect().unwrap();

    // (Cmd #2) Attach paths.
    client.paths_attach(PATHS).unwrap();

    assert_stop(NAME, server);
}

#[test]
fn terminate_with_failure() {
    const NAME: &str = "terminate-with-failure";

    // Start the server.
    let server = server_cmd!(start: NAME).spawn().unwrap();
    thread::sleep(Duration::from_secs(1));

    // Stop the server with failure.
    assert_stop_with_failure(NAME, server);
}

#[test]
fn terminate_via_signals() {
    const NAME: &str = "terminate-via-signals";

    // Start the server.
    let server = server_cmd!(start: NAME).spawn().unwrap();
    thread::sleep(Duration::from_secs(1));

    // Stop the server via signals.
    assert_stop_via_signals(server);
}

#[test]
fn invalid_name() {
    // The server should not start with an empty name.
    assert!(!server_cmd!(start (no-prefix): "")
        .spawn()
        .unwrap()
        .wait()
        .unwrap()
        .success());

    // The server should not start with a name containing '/'.
    assert!(!server_cmd!(start: "invalid-/name-/with-/slash")
        .spawn()
        .unwrap()
        .wait()
        .unwrap()
        .success());
}

/// The path to the server binary.
const PATH: &str = env!(concat!("CARGO_BIN_EXE_", env!("CARGO_PKG_NAME")));

/// The prefix for the integration test.
const PREFIX: &str = "integration-test";

/// Construct a queue name for the integration test.
fn queue_name(name: &str) -> String {
    format!("{}-{}", PREFIX, name)
}

/// Construct a table name for the integration test.
fn table_name(name: &str, mangled_path: &str) -> String {
    format!("{}-{}-{}", PREFIX, name, mangled_path)
}

/// Construct a route name for the integration test.
fn route_name(name: &str) -> String {
    format!("{}-{}", PREFIX, name)
}

/// Stop the server & assert that it succeeded.
fn assert_stop(name: &str, mut proc: Child) {
    server_cmd!(stop: name).spawn().unwrap().wait().unwrap();

    for _ in 0..1000 {
        match proc.try_wait() {
            Ok(Some(status)) => assert!(status.success()),
            _ => thread::sleep(Duration::from_millis(50)),
        }
    }

    proc.kill().unwrap();
}

/// Stop the server with failure & assert that it failed.
fn assert_stop_with_failure(name: &str, mut proc: Child) {
    server_cmd!(stop: name, "-F")
        .spawn()
        .unwrap()
        .wait()
        .unwrap();

    for _ in 0..1000 {
        match proc.try_wait() {
            Ok(Some(status)) => assert!(!status.success()),
            _ => thread::sleep(Duration::from_millis(50)),
        }
    }

    proc.kill().unwrap();
}

/// Stop the server via signals & assert that it succeeded.
fn assert_stop_via_signals(mut proc: Child) {
    for _ in 0..1000 {
        // Send SIGINT & SIGTERM to the server.
        //
        // Safety: sending signals to a running process is safe.
        unsafe {
            libc::kill(proc.id() as _, libc::SIGINT);
            libc::kill(proc.id() as _, libc::SIGTERM);
        }
        match proc.try_wait() {
            Ok(Some(status)) => assert!(status.success()),
            _ => thread::sleep(Duration::from_millis(50)),
        }
    }

    proc.kill().unwrap();
}
